<?php

/**
 * Klasa walidująca poprawność wpisanego tokenu
 *
 * @category	Core
 * @package		Core_Validate
 * @copyright	Copyright (c) 2008 Kuśmierz Adam
 */

class Core_Validate_Token extends Zend_Validate_Abstract
{
	const INVALID = 'TokenIsNotValid';
	const EXPIRED = 'TokenExpired';
	const USED = 'TokenHasBeenUsed';
	
	/**
	 * @var array
	*/
	protected $_messageTemplates = array(
		self::INVALID => 'Token is not valid',
		self::EXPIRED => 'Token expired',
		self::USED => 'Token has been used'
	);
	
	/**
	 * Seed for token
	 *
	 * @var string
	*/
	protected $_seed = ''; // @TODO default hardcoded, change it in Filter_Token too!
	
	/**
	 * Hash for token
	 *
	 * @var string
	*/
	protected $_hash;
	
	/**
	 * Callback to Zend_Db_Table method, it should checks if selected token exists (or not)
	 *
	 * @var array
	*/
	protected $_callback;
	
	/**
	 * Sets validator options
	 *
	 * @param string $hash Hash for Token
	 * @param array $model
	 * @throws Zend_Validate_Exception
	 * @return void
	 */
	public function __construct($hash, $model = array())
	{
		// validating input data
		if (!is_string($hash) || empty($hash))
			throw new Zend_Validate_Exception('Hash must be non-empty string');
		
		if (!is_null($model)) {
			if (!is_array($model) || empty($model) || count($model) !== 2)
				throw new Zend_Validate_Exception('Model should be an array');
			$modelClass = reset($model);
			$method = end($model);
			unset($model);
		}
		
		if (isset($method) && isset($modelClass) && (!is_null($modelClass) || !is_null($method))) {
			$method = (string) $method;
			
			if (!($modelClass instanceof Zend_Db_Table_Abstract))
				throw new Zend_Validate_Exception('The class "' . get_class($modelClass) . '" isn\'t required instance of Zend_Db_Table');
			
			if (!method_exists($modelClass, $method))
				throw new Zend_Validate_Exception('The method "' . $method . '()" doesn\'t exists in "' . get_class($modelClass) . '" class');
			
			$this->_callback = array($modelClass, $method);
		}
		$this->setHash($hash);
	}
	
	/**
	 * Returns hash
	 *
	 * @return string
	 */
	public function getHash()
	{
		return $this->_hash;
	}
	
	/**
	 * Sets hash for token
	 *
	 * @param  string $hash
	 * @return Core_Validate_Password Provides a fluent interface
	 */
	public function setHash($hash)
	{
		$this->_hash = (string) $hash;
		return $this;
	}
	
	/**
	 * Returns seed
	 *
	 * @return string
	 */
	public function getSeed()
	{
		return $this->_seed;
	}
	
	/**
	 * Sets seed to hash token
	 *
	 * @param  string $seed
	 * @return Core_Validate_Password Provides a fluent interface
	 */
	public function setSeed($seed)
	{
		$this->_seed = (string) $seed;
		return $this;
	}
	
	/**
	 * Defined by Zend_Validate_Interface
	 *
	 * Returns true if and only if given token is correct
	 *
	 * @param  string $value
	 * @return boolean
	 */
	public function isValid($value)
	{
		$value = (string) $value;
		$this->_setValue($value);
		$timeModulus = 33554432; // 2^25
		
		$hash = 0;
		$time = 0;
		for ($i = 0; $i < 16; $i += 2) {
			$t1 = hexdec($value{$i});
			$t2 = hexdec($value{$i + 1});
			$hash <<= 4;
			$hash += ($t1 & 0xc) + ($t2 & 0x3);
			$time <<= 4;
			$time += ($t1 & 0x3) + ($t2 & 0xc);
		}
		$hash = str_pad(dechex($hash), 8, 0, STR_PAD_LEFT);
		$hash .= substr($value, 16);
		
		// a jak na big-endian to się zachowa?
		$microtime = ($time >> 25);
		$timeCongruent = $time & 0x01ffffff;
		$realTime = time();
		$realTimeCongruent = $realTime % $timeModulus;
		$carry = round(($realTimeCongruent - $timeCongruent) / $timeModulus);
		
		$timeTime = ((floor($realTime / $timeModulus) + $carry) * $timeModulus) + $timeCongruent;
		$timeSigned = unpack('l', pack('L', $time));
		$timeSigned = reset($timeSigned);
		
		if (abs(time() - $timeTime) > 1800) {
			$this->_error(self::EXPIRED);
			Zend_Registry::get('Zend_Log')->info('Token "' . $value . '" expired ' . abs(time() - $timeTime) . 's ago');
			return false;
		}
		
		if ($hash !== md5(md5($this->getHash() . $this->getSeed()) . abs($timeSigned))) {
			$this->_error(self::INVALID);
			Zend_Registry::get('Zend_Log')->info('time: ' . $time . ' (' . microtime(true) . '/' . $timeTime . '.' . $microtime . '); diff: ' . abs(time() - $timeTime));
			Zend_Registry::get('Zend_Log')->info('token: ' . $value);
			Zend_Registry::get('Zend_Log')->info('session&Seed: ' . $this->getHash() . $this->getSeed());
			Zend_Registry::get('Zend_Log')->info('hash: ' . $hash);
			Zend_Registry::get('Zend_Log')->info('validHash: ' . md5(md5($this->getHash() . $this->getSeed()) . abs($timeSigned)));
			return false;
		}
		
		if (!is_null($this->_callback) && !call_user_func($this->_callback, $value)) {
			$this->_error(self::USED);
			Zend_Registry::get('Zend_Log')->info('Token "' . $value . '" used twice');
			return false;
		}
		
		return true;
	}
}